﻿#region License

// Another free, open solution by someone who gets it.
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#endregion

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;

using Patterns.SafeCode;
using Patterns.Text;

namespace Patterns.Reflection
{
	/// <summary>
	/// 	Provides a simple bridge between the Type and String classes (useful for configuration scenarios)
	/// </summary>
	[DebuggerDisplay("{FriendlyName}")]
	[TypeConverter(typeof (TypeNameConverter))]
	public class TypeName
	{
		private const string _namePatternKey = "class";
		private const string _typeArgsPatternKey = "typeArgs";

		private const string _typePatternFormat =
			@"(?<{0}>[^`\s\<\[{{]+)\s*([`\d]+)?\s*([\<\[{{]{{1}}\s*(?<{1}>[^\>\]}}]+)\s*[\>\]}}]{{1}})?";

		private static readonly Lazy<IDictionary<char, IDictionary<string, Type>>> _alphaMenu =
			new Lazy<IDictionary<char, IDictionary<string, Type>>>(BuildAlphaMenu);

		private static readonly IDictionary<string, Type> _keywordTypes =
			new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase)
			{
				{ "object", typeof (object) },
				{ "string", typeof (string) },
				{ "bool", typeof (bool) },
				{ "sbyte", typeof (sbyte) },
				{ "byte", typeof (byte) },
				{ "char", typeof (char) },
				{ "short", typeof (short) },
				{ "ushort", typeof (ushort) },
				{ "int", typeof (int) },
				{ "uint", typeof (uint) },
				{ "long", typeof (long) },
				{ "ulong", typeof (ulong) },
				{ "float", typeof (float) },
				{ "double", typeof (double) },
				{ "decimal", typeof (decimal) }
			};

		private static readonly CompiledRegex _typeArgPattern = @"\s*,\s*";
		private static readonly CompiledRegex _identifierPattern = @"\w+";
		private static readonly CompiledRegex _namePattern = string.Format(@"(?<{0}>[^\.]+)$", _namePatternKey);

		private static readonly CompiledRegex _typePattern = string.Format(_typePatternFormat, _namePatternKey,
		                                                                   _typeArgsPatternKey);

		///<summary>
		///	Gets the friendly (display) name of the type.
		///</summary>
		public string FriendlyName
		{
			get { return Type != null ? Type.GetDisplayName() : Name; }
		}

		///<summary>
		///	Gets the name of the type.
		///</summary>
		public string Name{ get; set; }

		///<summary>
		///	Gets the system type information.
		///</summary>
		public Type Type{ get; set; }

		///<summary>
		///	Implicitly converts strings to <see cref = "TypeName" /> instances.
		///</summary>
		///<param name = "typeName">The string value.</param>
		///<returns>The <see cref = "TypeName" /> instance.</returns>
		///<exception cref = "ArgumentNullException">Thrown if the <paramref name = "typeName" /> parameter is null.</exception>
		public static implicit operator TypeName(string typeName)
		{
			if (string.IsNullOrEmpty(typeName)) throw new ArgumentNullException("typeName");

			return new TypeName { Name = typeName, Type = FindType(typeName) };
		}

		///<summary>
		///	Implicitly converts system types into <see cref = "TypeName" /> instances.
		///</summary>
		///<param name = "type">The system type.</param>
		///<returns>The <see cref = "TypeName" /> instance.</returns>
		///<exception cref = "ArgumentNullException">Thrown if the <paramref name = "type" /> parameter is null.</exception>
		public static implicit operator TypeName(Type type)
		{
			if (type == null) throw new ArgumentNullException("type");

			return new TypeName { Name = type.AssemblyQualifiedName, Type = type };
		}

		///<summary>
		///	Implicitly converts <see cref = "TypeName" /> instances into system type instances.
		///</summary>
		///<param name = "typeName">The <see cref = "TypeName" /> instance.</param>
		///<returns>The system type instance.</returns>
		///<exception cref = "ArgumentNullException">Thrown if the <paramref name = "typeName" /> parameter is null.</exception>
		public static implicit operator Type(TypeName typeName)
		{
			if (typeName == null) throw new ArgumentNullException("typeName");

			return typeName.Type;
		}

		///<summary>
		///	Implicitly converts <see cref = "TypeName" /> instances into string instances.
		///</summary>
		///<param name = "typeName">The <see cref = "TypeName" /> instance.</param>
		///<returns>The string instance.</returns>
		///<exception cref = "ArgumentNullException">Thrown if the <paramref name = "typeName" /> parameter is null.</exception>
		public static implicit operator string(TypeName typeName)
		{
			if (typeName == null) throw new ArgumentNullException("typeName");

			return typeName.Name;
		}

		private static Type FindType(string typeName)
		{
			Type type = Try.Get(() => Type.GetType(typeName, false, true));
			if (type != null) return type;

			if (!_identifierPattern.IsMatch(typeName)) return null;

			var context = new TypeInvestigationContext(typeName);

			FindType(context);

			return context.ResolvedType;
		}

		private static void FindType(TypeInvestigationContext context)
		{
			context.ResolvedGenericArgs = string.IsNullOrEmpty(context.Request.GenericArgs)
			                              	? Enumerable.Empty<Type>()
			                              	: _typeArgPattern.Split(context.Request.GenericArgs).Select(FindType);

			ResolveType(context);
		}

		private static void ResolveType(TypeInvestigationContext context)
		{
			GetCandidateTypes(context);

			if (context.ResolvedType != null) return;

			Type[] resolvedTypeArgs = context.ResolvedGenericArgs.ToArray();

			if (resolvedTypeArgs.Length > 0)
			{
				Type matchingTypeDefinition = context.CandidateTypes
					.Where(type => type.IsGenericTypeDefinition)
					.Select(type => new { type, args = type.GetGenericArguments() })
					.Where(t => t.args.Length == resolvedTypeArgs.Length)
					.Select(t => t.type).FirstOrDefault();

				context.ResolvedType = matchingTypeDefinition != null
				                       	? matchingTypeDefinition.MakeGenericType(resolvedTypeArgs)
				                       	: null;
			}
			else if (context.CandidateTypes != null) context.ResolvedType = context.CandidateTypes.FirstOrDefault();
		}

		private static void GetCandidateTypes(TypeInvestigationContext context)
		{
			context.ResolvedType = _keywordTypes[context.Request.BasicName];

			if (context.ResolvedType != null) return;

			IDictionary<string, string> scrubbedTypeData = _namePattern.DictionaryMatch(context.Request.BasicName);
			string className = scrubbedTypeData[_namePatternKey];

			char firstLetter = className.ToLower().FirstOrDefault();

			if (!_alphaMenu.Value.ContainsKey(firstLetter)) return;

			IDictionary<string, Type> typeNames = _alphaMenu.Value[firstLetter];
			CompiledRegex pattern = string.Format(@"(?i)(^|\.){0}$", Regex.Escape(context.Request.BasicName));

			context.CandidateTypes = typeNames.Where(t => pattern.IsMatch(t.Key)).Select(t => t.Value);
		}

		private static IDictionary<char, IDictionary<string, Type>> BuildAlphaMenu()
		{
			IEnumerable<Type> allTypes = AppDomain.CurrentDomain.GetAssemblies()
				.Select(a => Try.Get(() => a.GetTypes().AsEnumerable()))
				.Aggregate((s1, s2) => s1.Concat(s2));

			IDictionary<char, IDictionary<string, Type>> alphaMenu = allTypes
				.Where(t => !string.IsNullOrEmpty(t.FullName))
				.Select(t => new TypeAlphaMenuItem { NameDetails = _typePattern.DictionaryMatch(t.FullName), ItemType = t })
				.GroupBy(GetFirstNameCharacter)
				.ToDictionary(g => g.Key, GenerateAlphaSubMenu);

			return alphaMenu;
		}

		private static Type GetAlphaMenuItemByName(IEnumerable<TypeAlphaMenuItem> items, string name)
		{
			Type result = items.Where(i => i.Name == name).Select(i => i.ItemType).FirstOrDefault();
			return result;
		}

		private static IDictionary<string, Type> GenerateAlphaSubMenu(IEnumerable<TypeAlphaMenuItem> items)
		{
			IEnumerable<string> keys = items.Select(i => i.Name).Distinct(StringComparer.OrdinalIgnoreCase);
			return keys.ToDictionary(name => name, name =>
			{
				IEnumerable<TypeAlphaMenuItem> targetItems = items;
				return GetAlphaMenuItemByName(targetItems, name);
			});
		}

		private static string GetFirstName(string name)
		{
			IDictionary<string, string> firstNameMatch = _namePattern.DictionaryMatch(name);
			return firstNameMatch[_namePatternKey];
		}

		private static char GetFirstNameCharacter(TypeAlphaMenuItem item)
		{
			string name = GetFirstName(item.Name);
			char firstChar = name.ToLower().FirstOrDefault();
			return firstChar;
		}

		#region Nested type: TypeAlphaMenuItem

		private class TypeAlphaMenuItem
		{
			public IDictionary<string, string> NameDetails{ private get; set; }
			public Type ItemType{ get; set; }

			public string Name
			{
				get { return NameDetails[_namePatternKey] ?? "_unspecified"; }
			}
		}

		#endregion

		#region Nested type: TypeInvestigationContext

		private class TypeInvestigationContext
		{
			public TypeInvestigationContext(string requestedTypeName)
			{
				Request = new TypeInvestigationRequest(_typePattern.DictionaryMatch(requestedTypeName));
			}

			public TypeInvestigationRequest Request{ get; private set; }
			public IEnumerable<Type> ResolvedGenericArgs{ get; set; }
			public Type ResolvedType{ get; set; }
			public IEnumerable<Type> CandidateTypes{ get; set; }
		}

		#endregion

		#region Nested type: TypeInvestigationRequest

		private class TypeInvestigationRequest
		{
			public TypeInvestigationRequest(IDictionary<string, string> match)
			{
				BasicName = match[_namePatternKey];
				GenericArgs = match[_typeArgsPatternKey];
			}

			public string BasicName{ get; private set; }
			public string GenericArgs{ get; private set; }
		}

		#endregion

		#region Nested type: TypeNameConverter

		///<summary>
		///	Provides a <see cref = "TypeConverter" /> implementation for the <see cref = "TypeName" /> type.
		///</summary>
		public class TypeNameConverter : TypeConverter
		{
			private static readonly IEnumerable<Type> _validTypes = new[] { typeof (string), typeof (Type) };

			/// <summary>
			/// 	Returns whether this converter can convert an object of the given type to the type of this converter, using the specified context.
			/// </summary>
			/// <returns>
			/// 	true if this converter can perform the conversion; otherwise, false.
			/// </returns>
			/// <param name = "context">An <see cref = "T:System.ComponentModel.ITypeDescriptorContext" /> that provides a format context. </param>
			/// <param name = "sourceType">A <see cref = "T:System.Type" /> that represents the type you want to convert from. </param>
			public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
			{
				return _validTypes.Any(t => t == sourceType);
			}

			/// <summary>
			/// 	Returns whether this converter can convert the object to the specified type, using the specified context.
			/// </summary>
			/// <returns>
			/// 	true if this converter can perform the conversion; otherwise, false.
			/// </returns>
			/// <param name = "context">An <see cref = "T:System.ComponentModel.ITypeDescriptorContext" /> that provides a format context. </param>
			/// <param name = "destinationType">A <see cref = "T:System.Type" /> that represents the type you want to convert to. </param>
			public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
			{
				return _validTypes.Any(t => t == destinationType);
			}

			/// <summary>
			/// 	Converts the given object to the type of this converter, using the specified context and culture information.
			/// </summary>
			/// <returns>
			/// 	An <see cref = "T:System.Object" /> that represents the converted value.
			/// </returns>
			/// <param name = "context">An <see cref = "T:System.ComponentModel.ITypeDescriptorContext" /> that provides a format context. </param>
			/// <param name = "culture">The <see cref = "T:System.Globalization.CultureInfo" /> to use as the current culture. </param>
			/// <param name = "value">The <see cref = "T:System.Object" /> to convert. </param>
			/// <exception cref = "T:System.NotSupportedException">The conversion cannot be performed. </exception>
			public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
			{
				if (value is string) return (TypeName) (string) value;
				if (value is Type) return (TypeName) (Type) value;
				return null;
			}

			/// <summary>
			/// 	Converts the given value object to the specified type, using the specified context and culture information.
			/// </summary>
			/// <returns>
			/// 	An <see cref = "T:System.Object" /> that represents the converted value.
			/// </returns>
			/// <param name = "context">An <see cref = "T:System.ComponentModel.ITypeDescriptorContext" /> that provides a format context. </param>
			/// <param name = "culture">A <see cref = "T:System.Globalization.CultureInfo" />. If null is passed, the current culture is assumed. </param>
			/// <param name = "value">The <see cref = "T:System.Object" /> to convert. </param>
			/// <param name = "destinationType">The <see cref = "T:System.Type" /> to convert the <paramref name = "value" /> parameter to. </param>
			/// <exception cref = "T:System.ArgumentNullException">The <paramref name = "destinationType" /> parameter is null. </exception>
			/// <exception cref = "T:System.NotSupportedException">The conversion cannot be performed. </exception>
			public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value,
			                                 Type destinationType)
			{
				if (destinationType == typeof (string)) return (string) (TypeName) value;
				if (destinationType == typeof (Type)) return (Type) (TypeName) value;
				return null;
			}
		}

		#endregion
	}
}